package com.niodata.dp.expression.function.date;

import com.googlecode.aviator.runtime.function.AbstractFunction;
import com.googlecode.aviator.runtime.function.FunctionUtils;
import com.googlecode.aviator.runtime.type.AviatorObject;
import com.googlecode.aviator.runtime.type.AviatorRuntimeJavaType;
import com.googlecode.aviator.runtime.type.AviatorType;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.tuple.Pair;

public class RangeDateFunction extends AbstractFunction {


  @Override
  public String getName() {
    return "range_date";
  }

  @Override
  public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2,
        AviatorObject arg3, AviatorObject arg4, AviatorObject arg5, AviatorObject arg6) {
    String outputFormatStr = FunctionUtils.getStringValue(arg1, env);
    String inputFormatStr = FunctionUtils.getStringValue(arg2, env);
    String step = FunctionUtils.getStringValue(arg5, env);
    boolean filterFutureTime = FunctionUtils.getBooleanValue(arg6, env);
    SimpleDateFormat inputFormat = new SimpleDateFormat(inputFormatStr);

    Date start;
    Date end;
    try {
      if (arg3.getAviatorType() == AviatorType.JavaType) {
        Object javaObject = FunctionUtils.getJavaObject(arg3, env);
        if (javaObject instanceof Date) {
          start = (Date) javaObject;
        } else {
          start = inputFormat.parse(javaObject.toString());
        }

      } else {
        String startStr = FunctionUtils.getStringValue(arg3, env);
        start = inputFormat.parse(startStr);
      }
      if (arg4.getAviatorType() == AviatorType.JavaType) {
        Object javaObject = FunctionUtils.getJavaObject(arg4, env);
        if (javaObject instanceof Date) {
          end = (Date) javaObject;
        } else {
          end = inputFormat.parse(javaObject.toString());
        }
      } else {
        String endStr = FunctionUtils.getStringValue(arg4, env);
        end = inputFormat.parse(endStr);
      }
      LocalDateTime startDateTime =
            start.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
      LocalDateTime endDateTime = end.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();

      Pair<TimeUnit, Long> pair = parseTimeStep(step);
      TimeUnit unit = pair.getKey();
      long stepNum = pair.getValue();

      TimeUnit minTimeUnit = getMinTimeUnit(inputFormatStr);
      if (startDateTime.equals(endDateTime) && minTimeUnit.ordinal() > unit.ordinal()) {
        switch (minTimeUnit) {
          case YEARS:
            endDateTime = endDateTime.plusYears(1);
            break;
          case MONTHS:
            endDateTime = endDateTime.plusMonths(1);
            break;
          case DAYS:
            endDateTime = endDateTime.plusDays(1);
            break;
          case HOURS:
            endDateTime = endDateTime.plusHours(1);
            break;
          case MINUTES:
            endDateTime = endDateTime.plusMinutes(1);
            break;
          case SECONDS:
            endDateTime = endDateTime.plusSeconds(1);
            break;
          default:
            break;
        }
      }
      List<String> result =
            generateRangeDateList(startDateTime, endDateTime, unit, stepNum, outputFormatStr,
                  filterFutureTime);
      return AviatorRuntimeJavaType.valueOf(result);
    } catch (ParseException e) {
      throw new RuntimeException(e);
    }
  }

  private List<String> generateRangeDateList(LocalDateTime start, LocalDateTime end,
        TimeUnit stepUnit, long stepNum, String dateTimeFormat, boolean filterFutureTime) {
    List<String> result = new ArrayList<>(64);
    LocalDateTime now = LocalDateTime.now();
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateTimeFormat);
    LocalDateTime temp = start;
    while (temp.isBefore(end) && (!filterFutureTime || !temp.isAfter(now))) {
      result.add(formatter.format(temp));
      switch (stepUnit) {
        case YEARS:
          temp = temp.plusYears(stepNum);
          break;
        case MONTHS:
          temp = temp.plusMonths(stepNum);
          break;
        case DAYS:
          temp = temp.plusDays(stepNum);
          break;
        case HOURS:
          temp = temp.plusHours(stepNum);
          break;
        case MINUTES:
          temp = temp.plusMinutes(stepNum);
          break;
        default:
          break;
      }
    }
    return result;
  }

  /**
   * 从日期格式化范式中找到最小时间单位，例如yyy-MM 最小时间单位为月，yyyy-MM-dd 最小时间单位为日. 暂不支持星期week
   *
   * @param dateFormat 日期格式化范式
   * @return 最小时间单位
   */
  private TimeUnit getMinTimeUnit(String dateFormat) {
    if (dateFormat.contains("s")) {
      return TimeUnit.SECONDS;
    } else if (dateFormat.contains("m")) {
      return TimeUnit.MINUTES;
    } else if (dateFormat.matches(".*[HhKk].*")) {
      return TimeUnit.HOURS;
    } else if (dateFormat.matches(".*[DdFEu].*")) {
      return TimeUnit.DAYS;
    } else if (dateFormat.matches(".*[ML].*")) {
      return TimeUnit.MONTHS;
    } else if (dateFormat.matches(".*[Yy].*")) {
      return TimeUnit.YEARS;
    } else {
      return null;
    }
  }

  private Pair<TimeUnit, Long> parseTimeStep(String step) {
    TimeUnit unit;
    if (step.toUpperCase().contains(TimeUnit.YEARS.name())) {
      unit = TimeUnit.YEARS;
    } else if (step.toUpperCase().contains(TimeUnit.MONTHS.name())) {
      unit = TimeUnit.MONTHS;
    } else if (step.toUpperCase().contains(TimeUnit.DAYS.name())) {
      unit = TimeUnit.DAYS;
    } else if (step.toUpperCase().contains(TimeUnit.HOURS.name())) {
      unit = TimeUnit.HOURS;
    } else if (step.toUpperCase().contains(TimeUnit.MINUTES.name())) {
      unit = TimeUnit.MINUTES;
    } else {
      throw new RuntimeException("Unsupported TimeStepUnit: " + step);
    }
    long stepNum = Long.valueOf(step.toUpperCase().replace(unit.name(), "").trim());
    return Pair.of(unit, stepNum);
  }

  public enum TimeUnit {
    SECONDS,
    MINUTES,
    HOURS,
    DAYS,
    MONTHS,
    YEARS;
  }
}
